/*
	
	typism - http://code.google.com/p/typism/
	
	Copyright (c) 2008, 2009 Juraj Sukop
	
	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:
	
	The above copyright notice and this permission notice shall be included in
	all copies or substantial portions of the Software.
	
	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
	THE SOFTWARE.
	
*/


Editor = {
	
	init: function() {
		Interface.init();
	}
	
};

Interface = {
	
	init: function() {
		var points = [];
		
		var form = document.forms[0];
		var reset = Textarea.init();
		var parent = document.getElementById('editor-font-parent'); // note: has to have .innerHTML, i.e. SVG font source
		form.onsubmit = Interface.copy(parent, reset);
		
		var font = document.getElementsByTagName('font')[0];
		var workplace = document.getElementById('editor-font-workplace');
		var scroller = document.getElementById('editor-font-scroller');
		var container = document.getElementById('editor-font-container'); // Firefox bug: wont wrap around the child, see http://code.google.com/p/typism/issues/detail?id=43
		var circle = document.getElementById('editor-font-circle');
		var canvas = document.getElementById('editor-font-canvas');
		var path = document.getElementById('editor-font-path');
		var marquee = document.getElementById('editor-font-marquee');
		var charset = document.getElementById('editor-font-charset');
		
		var glyph = Glyph.dummy();
		var a = canvas.getScreenCTM().inverse().a;
		var frame = Frame.init(font, document.getElementById('editor-font-guide'), document.getElementById('editor-font-frame'), a);
		var handler = Handler.init(document.getElementById('editor-font-handler'), a);
		circle.setAttribute('transform', 'scale(' + a + ')');
		path.setAttribute('stroke-width' , a);
		
		var code = Code.init(document.getElementById('editor-font-code'), workplace, glyph, canvas, path, points);
		
		var zoom = Tools.zoom(canvas, scroller, path, circle, frame, handler);
		var pan = Tools.pan(scroller);
		var move = Tools.move(glyph, canvas, path, circle, frame, handler, points, marquee);
		var draw = Tools.draw(glyph, canvas, path, circle, points);
		
		var tools = {
			current: null,
			all: {
				text: {
					icon: document.getElementById('editor-icon-text'),
					pane: document.getElementById('editor-pane-text')
				},
				move: {
					icon: document.getElementById('editor-icon-move'),
					pane: document.getElementById('editor-pane-font'),
					callback: move
				},
				draw: {
					icon: document.getElementById('editor-icon-draw'),
					pane: document.getElementById('editor-pane-font'),
					callback: draw.callback,
					reset: draw.reset
				},
				code: {
					icon: document.getElementById('editor-icon-code'),
					pane: document.getElementById('editor-pane-font'),
				},
				test: {
					icon: document.getElementById('editor-icon-test'),
					pane: document.getElementById('editor-pane-test'),
				},
				copy: {
					icon: document.getElementById('editor-icon-copy'),
					pane: document.getElementById('editor-pane-copy')
				}
			}
		};
		for (var key in tools.all) {
			var tool = tools.all[key]
			tool.icon.onclick = Interface.change(tools, tool, path, circle, frame, handler, code, points);
			tool.icon.removeAttribute('id');
			tool.pane.removeAttribute('id');
			tool.pane.className = 'pane';
		}
		var hash = location.hash.slice(1);
		if ((hash != 'move') && (hash != 'draw') && (hash != 'code') && (hash != 'test') && (hash != 'copy')) {
			hash = 'text';
		}
		tools.current = tools.all[hash == 'text' ? 'move' : 'text'];
		tools.all[hash].icon.onclick();
		
		workplace.oncontextmenu = function() { return false };
		container.addEventListener(Utils.agent.mozilla ? 'DOMMouseScroll' : 'mousewheel', zoom, false);
		container.onmousedown = function(e) {
			if (e.which == 1) {
				tools.current.callback(e);
			} else {
				pan(e);
			}
			return false;
		};
		
		Flip.init(
			document.getElementById('editor-icon-hflip'),
			document.getElementById('editor-icon-vflip'),
			glyph, path, points
		);
		
		Charset.init(font, charset);
		charset.onclick = Charset.select(tools, font, glyph, canvas, path, circle, frame, handler, code, points);
	},
	
	change: function(tools, tool, path, circle, frame, handler, code, points) {
		return function(e) {
			if (tools.current != tool) {
				if (tools.current.pane != tool.pane) {
					tools.current.pane.removeAttribute('id');
					tool.pane.setAttribute('id', 'editor-pane-selected'); // bug: chrome wont show up until maquee, see http://code.google.com/p/typism/issues/detail?id=44
				}
				tools.current.icon.removeAttribute('id');
				tool.icon.setAttribute('id', 'editor-tool-selected');
				if (tools.current == tools.all.draw) {
					tools.all.draw.reset();
				}
				tools.current = tool;
				switch (tools.current) {
					case tools.all.move:
						Code.off(code);
						var selection = Utils.filter(points, Utils.selected);
						switch (selection.length) {
							case 0:
								Frame.off(frame);
								Handler.off(handler);
								Path.mode(path, circle, false);
								break;
							case 1:
								Frame.on(frame);
								Handler.on(handler, selection[0]);
								break;
							default:
								Frame.on(frame);
								break;
						}
						break;
					case tools.all.draw:
						Code.off(code);
						Frame.off(frame);
						Handler.off(handler);
						Path.mode(path, circle, true);
						break;
					case tools.all.code:
						Code.on(code);
						break;
				}
			}
		}
	},
	
	copy: function(parent, reset) {
		return function() {
			this.font.value = parent.innerHTML;
			reset();
			return true;
		}
	},
	
};

Textarea = {
	
	init: function() {
		var reset = Textarea.watermark(document.getElementById('editor-copy-summary'), 'Write a short summary to describe the changes.');
		Textarea.watermark(document.getElementById('editor-test-sample'), 'Amazingly few discotheques provide jukeboxes.');
		Textarea.grow(document.getElementsByTagName('textarea'));
		return reset;
	},
	
	grow: function(textareas) {
		for (var i = 0; i < textareas.length; i++) {
			var textarea = textareas[i];
			textarea.onkeydown = textarea.onkeyup = textarea.onpaste = function(textarea) {
				return function() {
					if (textarea.clientHeight < textarea.scrollHeight) {
						textarea.style.height = textarea.scrollHeight + 20 + 'px';
					}
				}
			}(textarea);
			textarea.onkeyup();
		}
	},
	
	watermark: function(textarea, text) {
		textarea.onfocus = function() {
			if (textarea.value == text) {
				textarea.value = '';
			}
		}
		textarea.onblur = function() {
			if (textarea.value == '') {
				textarea.value = text;
			}
		}
		textarea.value = text;
		return textarea.onfocus;
	}
	
};

Frame = {
	
	init: function(font, guide, node, a) {
	// Bug: Isn't the origin of a glyph a bit misaligned?
	// From specs: horiz-origin-x = "<number>" The X-coordinate in the font coordinate
	// system of the origin of a glyph to be used when drawing horizontally oriented text.
	// Let's say I have a point [200, 200] in an SVG wide and tall 1000px. And I want this point
	// to appear at cursor position when writing with the font. Should horiz-origin-x be 200 or -200?
	// horiz-origin-x = 200, horiz-origin-y = 200: chrome draws the glyph 200px too much to the right
	// horiz-origin-x =-200, horiz-origin-y =-200: chrome draws the glyph 200px too much to the top
	// Now what?
	// see http://code.google.com/p/typism/issues/detail?id=48
		var x = parseInt(font.getAttribute('horiz-origin-x'));
		var y = parseInt(font.getAttribute('horiz-origin-y'));
		var advance = parseInt(font.getAttribute('horiz-adv-x'));
		if (isNaN(x)) { x = 200; font.setAttribute('horiz-origin-x', x); }
		if (isNaN(y)) { y = 200; font.setAttribute('horiz-origin-y', y); }
		if (isNaN(advance)) { advance = 600; font.setAttribute('horiz-adv-x', advance); }
		var fontface = font.getElementsByTagName('font-face')[0];
		var capheight = parseInt(fontface.getAttribute('cap-height'));
		var xheight = parseInt(fontface.getAttribute('x-height'));
		var ascent = parseInt(fontface.getAttribute('ascent'));
		var descent = parseInt(fontface.getAttribute('descent'));
		if (isNaN(capheight)) { capheight = 600; fontface.setAttribute('cap-height', capheight); }
		if (isNaN(xheight)) { xheight = 450; fontface.setAttribute('x-height', xheight); }
		if (isNaN(ascent)) { ascent = 750; fontface.setAttribute('ascent', ascent); }
		if (isNaN(descent)) { descent = -150; fontface.setAttribute('descent', descent); }
		capheight += y;
		xheight += y;
		ascent += y;
		descent += y;
		var frame = {
			font: font,
			fontface: fontface,
			x: x,
			y: y,
			advance: advance,
			'cap-height': capheight,
			'x-height': xheight,
			ascent: ascent,
			descent: descent,
			visible: false,
			guide: guide,
			node: node,
			hit: null,
			points: [
				{ x: x + advance, y: 0, type: 'advance'}, // Note: check 'advance' first!
				{ x: x + advance, y: 1000, type: 'advance'},
				{ x: x, y: 0, type: 'x'},
				{ x: x, y: 1000, type: 'x'},
				{ x: 0, y: y, type: 'y'},
				{ x: 1000, y: y, type: 'y'},
				{ x: 0, y: capheight, type: 'cap-height'},
				{ x: 0, y: xheight, type: 'x-height'},
				{ x: 0, y: ascent, type: 'ascent'},
				{ x: 0, y: descent, type: 'descent'}
			],
			indexes: {
				'cap-height': 6,
				'x-height': 7,
				'ascent': 8,
				'descent': 9
			}
		};
		Frame.zoom(frame, a);
		return frame;
	},
	
	on: function(handler) {
		if (!handler.visible) {
			handler.visible = true;
			handler.guide.setAttribute('visibility', 'visible');
			handler.node.setAttribute('visibility', 'visible');
		}
	},
	
	off: function(handler) {
		if (handler.visible) {
			handler.visible = false;
			handler.guide.setAttribute('visibility', 'hidden');
			handler.node.setAttribute('visibility', 'hidden');
		}
	},
	
	redraw: function(frame, a) {
	// Bug: it became a bit slow to draw all this maybe a rewrite is needed?
		var d = 3 * a;
		var xa = frame.x + frame.advance;
		frame.guide.setAttribute('d',
			'M' + frame.x + ',0v1000' +
			'M0,' + frame.y + 'h1000' +
			'M' + xa + ',0v1000' +
			'M0,' + frame['cap-height'] + 'h1000' +
			'M0,' + frame['x-height'] + 'h1000' +
			'M0,' + frame.ascent + 'h1000' +
			'M0,' + frame.descent + 'h1000'
		)
		frame.node.setAttribute('d',
			'M' + frame.x + ',0v' + d + 'M' + frame.x + ',1000v-' + d + // bug:(?) chrome, safari and opera display the bottom handlers only 1px high. firefox displays it ok.
			'M0,' + frame.y + 'h' + d + 'M1000,' + frame.y + 'h-' + d +
			'M' + xa + ',0v' + d + 'M' + xa + ',1000v-' + d +
			'M0,' + frame['cap-height'] + 'h' + d +
			'M0,' + frame['x-height'] + 'h' + d +
			'M0,' + frame.ascent + 'h' + d +
			'M0,' + frame.descent + 'h' + d
		)
	},
	
	zoom: function(frame, a) {
		frame.guide.setAttribute('stroke-width', a * 1);
		frame.node.setAttribute('stroke-width', a * 3);
		Frame.redraw(frame, a);
	},
	
	hit: function(x, y, canvas, frame) {
		frame.hit = Utils.hit(x, y, canvas, frame.points);
		return frame.hit;
	},
	
	translate: function(canvas, origin, frame) {
		// bug: changes in frame.font.setAttribute(...) don't show up, see http://code.google.com/p/typism/issues/detail?id=47
		var e = 0;
		return function(c) {
			var current = Utils.position(canvas, c);
			switch (frame.hit.type) {
				case 'x':
					var d = current.x - origin.x - e;
					if (frame.x + d < 0) { d = -frame.x; } else if (frame.x + frame.advance + d > 1000) { d = 1000 - frame.x - frame.advance; }
					frame.x += d;
					frame.points[0].x = frame.points[1].x = frame.x + frame.advance;
					frame.points[2].x = frame.points[3].x = frame.x;
					frame.font.setAttribute('horiz-origin-x', frame.x);
					break;
				case 'y':
					var d = current.y - origin.y - e;
					if (frame.y + d < 0) { d = -frame.y; } else if (frame.y + d > 1000) { d = 1000 - frame.y; }
					frame.y += d;
					frame.points[4].y = frame.points[5].y = frame.y;
					frame.font.setAttribute('horiz-origin-y', frame.y);
					break;
				case 'advance':
					var d = current.x - origin.x - e;
					if (frame.advance + d < 0) { d = -frame.advance; } else if (frame.x + frame.advance + d > 1000) { d = 1000 - frame.x - frame.advance; }
					frame.advance += d;
					frame.points[0].x = frame.points[1].x = frame.x + frame.advance;
					frame.font.setAttribute('horiz-adv-x', frame.advance);
					break;
				case 'cap-height':
				case 'x-height':
				case 'ascent':
				case 'descent':
					var d = current.y - origin.y - e;
					var type = frame.hit.type;
					if (frame[type] + d < 0) { d = -frame[type]; } else if (frame[type] + d > 1000) { d = 1000 - frame[type]; }
					frame[type] += d;
					frame.points[frame.indexes[type]].y = frame[type];
					frame.fontface.setAttribute(type, frame[type] - frame.y);
					break;
			}
			e += d;
			Frame.redraw(frame, canvas.getScreenCTM().inverse().a); // bug: cache .a!
		}
	}
	
};

Handler = {
	
	init: function(node, a) {
		var handler = {
			x: 700,
			y: 100,
			visible: false,
			node: node
		};
		Handler.zoom(handler, a);
		return handler;
	},
	
	on: function(handler, point) {
		if (
			(point.type == 'Q') ||
			(point.type == 'C') ||
			(point.type == ' ' && point.prev.type == 'C')
		) {
			if (!handler.visible) {
				Handler.translate(handler, point);
				handler.visible = true;
				handler.node.setAttribute('visibility', 'visible');
			}
		}
	},
	
	off: function(handler) {
		if (handler.visible) {
			handler.visible = false;
			handler.node.setAttribute('visibility', 'hidden');
		}
	},
	
	zoom: function(handler, a) {
		handler.node.setAttribute('r', a * 8);
		handler.node.setAttribute('stroke-width', a * 4);
	},
	
	hit: function(x, y, canvas, handler) {
		var d = 14 * canvas.getScreenCTM().inverse().a;
		var dx = x - handler.x;
		var dy = y - handler.y;
		return dx * dx + dy * dy < d * d;
	},
	
	translate: function(handler, point) {
		handler.x = point.x;
		handler.y = point.y;
		handler.node.setAttribute('cx', point.x);
		handler.node.setAttribute('cy', point.y);
	},
	
	adjust: function(a, b, c, old_a, old_c) {
		var ab = Utils.distance(old_a, b);
		var ac = ab + Utils.distance(b, old_c);
		var ratio = ab / ac;
		b.x = a.x + (c.x - a.x) * ratio;
		b.y = a.y + (c.y - a.y) * ratio;
		b.node.setAttribute('x', b.x);
		b.node.setAttribute('y', b.y);
	},
	
	smooth: function(handler, point) {
		switch (point.type) {
			case 'C':
				Handler.adjust(point.prev.prev, point.prev, point, point.prev.prev, handler);
				break;
			case ' ':
				Handler.adjust(point, point.next, point.next.next, handler, point.next.next);
				break;
			case 'Q':
				Handler.adjust(point.prev.prev, point.prev, point, point.prev.prev, handler);
				Handler.adjust(point, point.next, point.next.next, handler, point.next.next);
				break;
		}
		Handler.translate(handler, point);
	}
	
};

Code = {
	
	init: function(node, workplace, glyph, canvas, path, points) {
		var textarea = node.getElementsByTagName('textarea')[0];
		var code = {
			node: node,
			textarea: textarea,
			d: textarea.value,
			workplace: workplace,
			glyph: glyph,
			path: path,
			visible: false,
			warning: false
		};
		var callback = textarea.onkeydown;
		textarea.oncut = Code.write(canvas, path, code, points);
		textarea.onkeydown = textarea.onkeyup = textarea.onpaste = function() {
			textarea.oncut();
			callback(); // force resize;
		};
		node.style.display = 'none';
		return code;
	},
	
	on: function(code) {
		if (!code.visible) {
			code.visible = true;
			code.node.style.display = 'block';
			code.workplace.style.display = 'none';
			Code.read(code);
		}
	},
	
	off: function(code) {
		if (code.visible) {
			code.visible = false;
			code.node.style.display = 'none';
			code.workplace.style.display = 'block';
		}
	},
	
	warning: function(code, state) {
		if (code.warning != state) {
			code.warning = state;
			code.textarea.className = state ? 'warning' : '';
		}
	},
	
	read: function(code) {
		var d = code.glyph.node.getAttribute('d');
		if (code.d != d) {
			if (d == undefined) {
				code.textarea.value = code.d = code.path.getAttribute('d');
			} else {
				code.textarea.value = code.d = d;
			}
			code.textarea.style.height = '1px';
			code.textarea.onkeydown();
		}
		code.textarea.select();
	},
	
	write: function(canvas, path, code, points) {
		return function() {
			if (code.d != code.textarea.value) {
				code.d = code.textarea.value;
				var node = Utils.build('path', null, {
					d: code.textarea.value
				});
				Path.parse(node, canvas, points);
				Path.render_safe(code.glyph, path, points);
				Code.warning(code, points.length == 0);
			}
		}
	}
	
};

Flip = {
	
	init: function(h_icon, v_icon, glyph, path, points) {
		h_icon.onclick = Flip.flip('x', glyph, path, points);
		v_icon.onclick = Flip.flip('y', glyph, path, points);
	},
	
	flip: function(axis, glyph, path, points) {
		return function(e) {
			for (var i = 0, n = points.length; i < n; i++) {
				var point = points[i];
				point[axis] = 1000 - point[axis];
				point.node.setAttribute(axis, point[axis]);
			}
			Path.render_safe(glyph, path, points);
		}
	}
	
};

Charset = {
	
	init: function(font, node) {
	// Note: U+FFFD is "replacement character", i.e. it is not "missing-glyph" or ".notdef"!
	// I didn't know how else to display missing-glyph. Any thoughts? So far, this only works in Chrome.
	// see http://code.google.com/p/typism/issues/detail?id=49
	// Bug: Even if U+0020 is not defined, it doesn't display missing-glyph
	// see http://code.google.com/p/typism/issues/detail?id=50
		var glyphs = font.getElementsByTagName('glyph');
		var set = {};
		for (var i = 0, n = glyphs.length; i < n; i++) {
			set[glyphs[i].getAttribute('unicode').charCodeAt(0).toString(16)] = null;
		}
		var missing = font.getElementsByTagName('missing-glyph').length > 0 ? '' : ' class="missing"';
		var html = '<li' + missing + '><div>&#xFFFD;</div><div>.notd</div></li>';
		html += Charset.range(0x20, 0x7f, set); // basic latin
		html += Charset.range(0xa0, 0x100, set); // latin-1
//		html += Charset.range(0x100, 0x180, set); // latin extended-a
//		html += Charset.range(0x180, 0x250, set); // latin extended-b
		node.innerHTML = html;
	},

	range: function(start, end, set) {
		var fragment = '';
		for (var code = start; code < end; code++) {
			var hex = code.toString(16);
			var missing = hex in set ? '' : ' class="missing"';
			hex = hex.toUpperCase();
			for (var i = 0, n = 4 - hex.length; i < n; i++) {
				hex = '0' + hex;
			}
			fragment += '<li' + missing + '><div>&#' + code + ';</div><div>' + hex + '</div></li>';
		}
		return fragment;
	},
	
	select: function(tools, font, glyph, canvas, path, circle, frame, handler, code, points) {
		return function(e) {
			var target = e.target;
			if (target.tagName.toLowerCase() == 'div') {
				target = target.parentNode;
			}
			var selected = document.getElementById('editor-charset-selected');
			if (selected) {
				selected.removeAttribute('id');
			}
			target.setAttribute('id', 'editor-charset-selected');
			var unicode = target.firstChild.firstChild.nodeValue;
			if (unicode == '\uFFFD') {
				unicode = '';
			}
			var current = glyph.node.tagName.toLowerCase() == 'missing-glyph' ? '' : glyph.node.getAttribute('unicode');
			if (unicode != current) {
				target.removeAttribute('class');
				Frame.off(frame);
				Handler.off(handler);
				switch (tools.current) {
					case tools.all.move:
						Path.mode(path, circle, false)
						break;
					case tools.all.draw:
						tools.all.draw.reset();
						Path.mode(path, circle, true);
						break;
				}
				Path.load(font, glyph, unicode, canvas, path, points);
				if (code.visible) {
					Code.read(code);
				}
			}
		}
	}
	
};

Tools = {
	
	zoom: function(canvas, scroller, path, circle, frame, handler) {
		var zoom = 0;
		var svg = canvas.ownerSVGElement;
		var width = parseInt(svg.getAttribute('width'));
		var height = parseInt(svg.getAttribute('height'));
		return function(e) {
			if (e.detail) {
				var delta = e.detail / -3;
			} else if (e.wheelDelta) {
				if (Utils.agent.chrome) {
					var delta = e.wheelDelta / 360;
				} else {
					var delta = e.wheelDelta / 120;
				}
			}
			zoom += delta;
			var point = Utils.position(canvas, e);
			var factor = Math.pow(1.2, zoom);
			svg.setAttribute('width', width * factor); // Firefox bug: should redraw first after scroll, not now
			svg.setAttribute('height', height * factor); // http://code.google.com/p/typism/issues/detail?id=40
			var matrix = canvas.getScreenCTM();
			if (Utils.agent.opera) { // Opera bug: wont update the matrix, see http://code.google.com/p/typism/issues/detail?id=41
				matrix.a = width * factor / svg.viewBox.baseVal.width;
				matrix.d = -height * factor / svg.viewBox.baseVal.height;
				//matrix.e = ?
				//matrix.f = ?
			}
			point = point.matrixTransform(matrix);
			scroller.scrollLeft += point.x - e.clientX;
			scroller.scrollTop += point.y - e.clientY;
			var inverse_a = matrix.inverse().a;
			path.setAttribute('stroke-width', inverse_a)
			Frame.zoom(frame, inverse_a);
			Handler.zoom(handler, inverse_a);
			if (Utils.agent.webkit) {
				circle.setAttribute('transform', 'scale(' + inverse_a + ')');
			} else {
				circle.transform.baseVal.getItem(0).setScale(inverse_a, inverse_a);
			}
			e.preventDefault();
		}
	},
	
	pan: function(scroller) {
		return function(o) {
			var callback = window.onmousemove;
			window.onmousemove = function(c) {
				scroller.scrollLeft += o.clientX - c.clientX;
				scroller.scrollTop += o.clientY - c.clientY;
				o = c;
			};
			window.onmouseup = function(e) { // Opera bug: wont fire, see http://code.google.com/p/typism/issues/detail?id=42
				window.onmousemove = callback;
				window.onmouseup = null;
			};
		}
	},
	
	move: function(glyph, canvas, path, circle, frame, handler, points, marquee) {
		return function(o) {
			var hit, selection;
			var origin = Utils.position(canvas, o);
			if (points.length > 0) {
				hit = Utils.hit(origin.x, origin.y, canvas, points);
				selection = Utils.filter(points, Utils.selected);
			} else {
				hit = null;
				selection = [];
			}
			if (hit && selection.length > 0) {
				if (!hit.selected) {
					Utils.filter(selection, Utils.invert);
					Utils.invert(hit);
					selection = [hit];
					Handler.off(handler);
					Handler.on(handler, hit);
				}
				window.onmousemove = Path.translate(
					glyph, canvas, path, points, selection, origin, handler.visible ? Handler.translate : null, [handler, hit]
				);
				window.onmouseup = function(e) {
					window.onmouseup = window.onmousemove = null;
				}
			} else if (handler.visible && Handler.hit(origin.x, origin.y, canvas, handler)) {
				window.onmousemove = Path.translate(
					glyph, canvas, path, points, selection, origin, Handler.smooth, [handler, selection[0]]
				);
				window.onmouseup = function(e) {
					window.onmouseup = window.onmousemove = null;
				}
			} else if (frame.visible && Frame.hit(origin.x, origin.y, canvas, frame)) {
				window.onmousemove = Frame.translate(
					canvas, origin, frame
				);
				window.onmouseup = function(e) {
					window.onmouseup = window.onmousemove = null;
				}
			} else {
				if (selection.length > 0) { // mode.editable == true
					Utils.filter(selection, Utils.invert);
				} else {
					Frame.on(frame);
					Path.mode(path, circle, true);
				}
				Handler.off(handler);
				window.onmousemove = function(c) {
					marquee.setAttribute('style', 'position:absolute; border:1px dotted #09f; ' +
						'left:' + Math.min(o.clientX, c.clientX) + 'px; ' +
						'top:' + Math.min(o.clientY, Math.max(c.clientY, 100)) + 'px; ' +
						'width:' + Math.abs(o.clientX - c.clientX) + 'px; ' +
						'height:' + Math.abs(o.clientY - Math.max(c.clientY, 100)) + 'px;'
					);
					var current = Utils.position(canvas, c);
					var left = Math.min(origin.x, current.x);
					var top = Math.min(origin.y, current.y);
					var right = Math.max(origin.x, current.x);
					var bottom = Math.max(origin.y, current.y);
					Utils.filter(points, function(point) {
						if ((left < point.x) && (point.x < right) && (top < point.y) && (point.y < bottom)) {
							if (!point.selected) {
								Utils.invert(point);
							}
						} else {
							if (point.selected) {
								Utils.invert(point);
							}
						}
					});
					var selection = Utils.filter(points, Utils.selected);
					if (selection.length == 1) {
						Handler.on(handler, selection[0]);
					} else {
						Handler.off(handler);
					}
				}
				window.onmouseup = function(e) {
					if (Utils.filter(points, Utils.selected).length == 0) {
						Frame.off(frame);
						Path.mode(path, circle, false);
					}
					marquee.setAttribute('style', '');
					window.onmouseup = window.onmousemove = null;
				}
			}
		}
	},
	
	draw: function(glyph, canvas, path, circle, points) {
		var contour = [];
		var edge = [];
		var length = 0;
		var zero = function() {
			length = 0;
			if (contour.length > 0) {
				contour.length = 0;
			}
		};
		var stop = function(e) {
			edge.length = 1;
		};
		var clean = function(e) {
			Utils.invert(edge[1]);
			edge.length = 1;
			window.onmouseup = stop;
		};
		var finish = function(e) {
			Utils.invert(edge[0]);
			window.onmousemove = window.onmouseup = null;
		};
		var end = function(hit) {
			Utils.filter(points, Utils.deselect);
			Utils.invert(hit);
			edge[0] = hit;
			zero();
			window.onmouseup = finish;
		};
		return {
			reset: function() {
				if (length > 0) {
					Path.remove(canvas, points, edge[0]);
					zero();
					window.onmousemove = window.onmouseup = null;
					Path.render_safe(glyph, path, points);
				}
			},
			callback: function(o) {
				var origin = Utils.position(canvas, o);
				if ((origin.x < 0) || (origin.x > 1000) || (origin.y < 0) || (origin.y > 1000)) {
					return;
				}
				if (length == 0) {
					var hit, point;
					if (points.length > 0) {
						hit = Utils.hit(origin.x, origin.y, canvas, points);
						if (!hit) {
							hit = Path.split(canvas, points, 0, points.length, origin);
							if (hit) {
								contour = Path.contour(hit);
							}
						}
					} else {
						hit = null;
					}
					if (hit) {
						if (hit.selected) {
							Path.remove(canvas, points, hit);
						} else {
							point = edge[2] = hit;
							contour = Path.contour(hit);
							Path.on(hit);
						}
						Path.render_safe(glyph, path, points);
					} else {
						point = origin;
					}
					if (point) {
						edge[0] = points[points.length] = Path.point(origin.x, origin.y, 'L', null, canvas);
						edge[0].node.style.display = 'none';
						edge[1] = points[points.length] = Path.point(point.x, point.y, 'L', edge[0], canvas);
						Utils.invert(edge[1]);
						length = 2;
						window.onmousemove = Path.translate(glyph, canvas, path, points, edge, origin, null, null);
						window.onmouseup = stop;
					}
				} else {
					var hit = Utils.hit(origin.x, origin.y, canvas, points.slice(-length, -2).concat(points[points.length - 1]));
//					if (!hit) {
//						hit = split(last) (without the before-last segment)
//					}
					if (hit) {
						if (hit == points[points.length - 1]) {
							if (length == 2) {
								if (contour.length > 0) {
									Path.remove(canvas, points, contour[0]);
									contour.length = 0;
								};
								Path.remove(canvas, points, hit);
								length = 0;
								window.onmousemove = window.onmouseup = null;
							} else {
								points[points.length - 1].type = edge[0].type;
								canvas.removeChild(edge[0].node);
								points.splice(-2, 1);
								points[points.length - 1].prev = points[points.length - 2];
								points[points.length - 2].next = points[points.length - 1];
								length -= 1;
								points.splice.apply(points, [0, 0].concat(points.slice(-length)));
								points.splice(-length, length);
								end(hit);
							}
						} else {
							if (hit.selected) {
								Path.remove(canvas, points, hit);
								length -= 1;
								if (length == 2) {
									Utils.invert(points[points.length - 1]);
								}
							} else {
								edge[1] = hit;
								Utils.filter(points, Utils.deselect);
								Utils.invert(hit);
								Path.on(hit);
							}
						}
					} else {
						if (contour.length > 0) {
							hit = Utils.hit(origin.x, origin.y, canvas, contour)
							if (!hit) {
								var index = points.indexOf(contour[0]);
								while (points[index - 1] == points[index].prev) {
									index -= 1;
								}
								hit = Path.split(canvas, points, index, contour.length, origin);
								if (hit) {
									contour = Path.contour(contour[0]);
								}
							}
						}
						if (hit) {
							Path.on(hit); // if already on-curve, do nothing
							Path.replace(canvas, points, contour, hit, length);
							end(hit);
						} else {
							if (edge[0].type == 'L') {
								edge[1] = Path.point(origin.x, origin.y, 'Q', edge[0].prev, canvas);
								edge[0].type = ' ';
							} else { // == ' '
								edge[1] = Path.point(origin.x, origin.y, ' ', edge[0].prev, canvas);
								if (edge[1].prev.type == 'Q') {
									edge[1].prev.type = 'C';
								} else {
									edge[0].type = 'L';
								}
							}
							Utils.filter(points, Utils.deselect);
							Utils.invert(edge[1]);
							points.splice(-2, 0, edge[1]);
							length += 1;
							window.onmouseup = clean;
						}
					}
					Path.render_safe(glyph, path, points);
				}
			}
		}
	}
	
};

Glyph = {
	
	dummy: function() {
		return {
			node: {
				tagName: '',
				setAttribute: function(a, b) {},
				getAttribute: function(a) {}
			}
		};
	}
	
};	

Path = {
	
	load: function(font, glyph, unicode, canvas, path, points) {
		var node;
		if (unicode == '') {
			node = font.getElementsByTagName('missing-glyph');
			if (node.length == 0) {
				node = Utils.build('missing-glyph', font, {
					d: ''
				});
			} else {
				node = node[0];
			}
		} else {
			node = font.querySelector('glyph[unicode="\\' + unicode.charCodeAt(0).toString(16) + '"]'); // chrome bug: '\\' -> '5C', see http://code.google.com/p/typism/issues/detail?id=45
			if (node == null) {
				var glyphs = font.getElementsByTagName('glyph');
				var sibling = null;
				for (var i = 0, n = glyphs.length; i < n; i++) {
					if (unicode < glyphs[i].getAttribute('unicode')) {
						sibling = glyphs[i];
						break;
					}
				}
				node = Utils.build('glyph', null, {
					unicode: unicode,
					d: ''
				});
				font.insertBefore(node, glyphs[i]);
			}
		}
		glyph.node = node;
		node = Utils.build('path', null, {
			d: node.getAttribute('d')
		});
		Path.parse(node, canvas, points);
		Path.render_safe(Glyph.dummy(), path, points);
	},
	
	parse: function(node, canvas, points) {
		var sibling = canvas.nextSibling;
		var parent = canvas.parentNode;
		parent.removeChild(canvas);
		for (var i = 0, n = points.length; i < n; i++) {
			canvas.removeChild(points[i].node);
		}
		points.length = 0;
		var point = function(x, y, type) {
			return Path.point(x, y, type, prev, canvas);
		};
		var close = function() {
			if (points[points.length - 1].x != start.x || points[points.length - 1].y != start.y) {
				points[points.length] = point(start.x, start.y, 'L');
			}
			prev = null;
		}
		var x, y, prev, start;
		var index = 0;
		for (var i = 0, n = node.pathSegList.numberOfItems; i < n; i++) {
			var seg = node.pathSegList.getItem(i);
			switch (seg.pathSegType) {
				case 0:
				case 1: // closepath
					continue;
				case 2: // moveto abs
					if (index != points.length) {
						close();
						index = points.length;
					}
					start = seg;
					break;
				case 4: // lineto abs
					prev = points[points.length] = point(seg.x, seg.y, 'L');
					break;
				case 6: // curveto cubic abs
					prev = points[points.length] = point(seg.x1, seg.y1, 'C');
					prev = points[points.length] = point(seg.x2, seg.y2, ' ');
					prev = points[points.length] = point(seg.x, seg.y, ' ');
					break;
				case 8: // curveto quadratic abs
					prev = points[points.length] = point(seg.x1, seg.y1, 'Q');
					prev = points[points.length] = point(seg.x, seg.y, ' ');
					break;
				case 12: // lineto horizontal abs
					prev = points[points.length] = point(seg.x, y, 'L');
					x = seg.x;
					continue;
				case 14: // lineto vertical abs
					prev = points[points.length] = point(x, seg.y, 'L');
					y = seg.y;
					continue;
			}
			x = seg.x;
			y = seg.y;
		}
		if (index != points.length) {
			close();
		}
		if (points.length == 1) {
			canvas.removeChild(points[0].node);
			points.length = 0;
		}
		parent.insertBefore(canvas, sibling);
	},
	
	point: function(x, y, type, prev, parent) {
		return new Path.proto(x, y, type, prev, parent);
	},
	
	proto: function(x, y, type, prev, parent) {
		this.x = x;
		this.y = y;
		this.type = type;
		this.selected = false;
		this.toString = function() {
//			return this.type + ' ' + this.x + ' ' + this.y + ' -- ';
			return this.type + ' -- prev: ' + this.prev.type + ', next: ' + this.next.type + '\n';
		};
		this.node = Utils.build('use', parent, {
			x: x,
			y: y,
			fill: '#fff',
			href: '#control-point-symbol'
		});
		if (prev) {
			this.prev = prev;
			this.next = prev.next;
			prev.next = this.next.prev = this;
		} else {
			this.prev = this.next = this;
		}
	},
	
	translate: function(glyph, canvas, path, points, selection, origin, callback, args) {
		var top, bottom, left, right;
		left = right = selection[0].x;
		top = bottom = selection[0].y;
		for (var i = 1, n = selection.length; i < n; i++) {
			var p = selection[i];
			if (p.x < left) { left = p.x; } else if (p.x > right) { right = p.x; }
			if (p.y < top) { top = p.y; } else if (p.y > bottom) { bottom = p.y; }
		}
		var ex = 0;
		var ey = 0;
		return function(c) {
			var current = Utils.position(canvas, c);
			var dx = current.x - origin.x - ex;
			var dy = current.y - origin.y - ey;
			if (left + dx < 0) { dx = -left; } else if (right + dx > 1000) { dx = 1000 - right; } // None: units-per-em
			if (top + dy < 0) { dy = -top; } else if (bottom + dy > 1000) { dy = 1000 - bottom; }
			Utils.filter(selection, function(point) {
				point.x += dx;
				point.y += dy;
				point.node.setAttribute('x', point.x);
				point.node.setAttribute('y', point.y);
			});
			left += dx; right += dx;
			top += dy; bottom += dy;
			ex += dx;
			ey += dy;
			if (callback) {
				callback.apply(this, args);
			}
			Path.render(glyph, path, points);
		}
	},
	
	contour: function(point) {
		var points = [];
		var next = point;
		do {
			points[points.length] = next;
			next = next.next;
		} while (next != point)
		return points;
	},
	
	reverse: function(contour) {
		contour.unshift(contour.pop());
		contour.unshift(contour.pop());
		contour.reverse();
		var last = contour.length - 1;
		var type = contour[last].type;
		for (var i = last; 1 <= i; i--) {
			contour[i].prev = contour[i - 1];
			contour[i].type = contour[i - 1].type;
			contour[i - 1].next = contour[i];
		}
		contour[0].prev = contour[last];
		contour[0].type = type;
		contour[last].next = contour[0];
		for (var i = last; 0 <= i; i--) {
			switch (contour[i].type) {
				case 'Q':
					contour[i].type = ' ';
					contour[i].prev.type = 'Q';
					i -= 1;
					break;
				case 'C':
					contour[i].type = ' ';
					contour[i].prev.type = ' ';
					contour[i].prev.prev.type = 'C';
					i -= 2;
					break;
			}
		}
	},
	
	replace: function(canvas, points, contour, hit, length) {
		var a, b;
		var ai = points.indexOf(hit);
		var bi = points.indexOf(contour[0]);
		if (bi < ai) {
			var temp = ai;
			ai = bi;
			bi = temp;
			a = contour[0];
			b = hit;
		} else {
			a = hit;
			b = contour[0];
		}
		var d = bi - ai;
		if (contour.length / 2 < d) { // ??? What to do, if equal? Compare path-lengths?
			var start = ai;
			while (points[start - 1] == points[start].prev) {
				start -= 1;
			}
			var end = start + contour.length - 1;
			//var end = points.indexOf(points[start].prev, bi);
			for (var i = start; i < ai; i++) {
				canvas.removeChild(points[i].node);
			}
			for (var i = bi + 1; i <= end; i++) {
				canvas.removeChild(points[i].node);
			}
			if (length < 3) {
				a.prev = b;
				a.type = 'L';
				b.next = a;
				points.splice(bi + 1, end - bi);
				points.splice(start, ai - start);
			} else {
				var last = points.slice(-length);
				if (b == hit) {
					Path.reverse(last);
				};
				last[0].prev = b;
				b.next = last[0];
				a.prev = last[last.length - 3];
				a.type = last[last.length - 2].type;
				last[last.length - 3].next = a;
				points.splice(bi + 1, end - bi);
				points.splice.apply(points, [start, ai - start].concat(last.slice(0, -2)));
			}
		} else {
			for (var i = ai + 1; i < bi; i++) {
				canvas.removeChild(points[i].node);
			}
			if (length < 3) {
				b.prev = a;
				b.type = 'L';
				a.next = b;
				points.splice(ai + 1, d - 1);
			} else {
				var last = points.slice(-length);
				if (a == hit) {
					Path.reverse(last);
				};
				last[0].prev = a;
				a.next = last[0];
				b.prev = last[last.length - 3];
				b.type = last[last.length - 2].type;
				last[last.length - 3].next = b;
				points.splice.apply(points, [ai + 1, d - 1].concat(last.slice(0, -2)));
			}
		}
		canvas.removeChild(points[points.length - 1].node);
		canvas.removeChild(points[points.length - 2].node);
		points.splice(-length, length);
	},
	
	on: function(point) {
		switch (point.type) {
			case 'Q':
				point.type = 'L';
				point.next.type = 'L';
				break;
			case 'C':
				point.type = 'L';
				point.next.type = 'Q';
				break;
			case ' ':
				if (point.prev.type == 'C') {
					point.prev.type = 'Q';
					point.next.type = 'L';
				}
				break;
		}
	},
	
	remove: function(canvas, points, point) {
		var index = points.indexOf(point);
		if (point.prev.prev == point) {
			canvas.removeChild(point.node);
			canvas.removeChild(point.prev.node);
			if (points[index - 1] == point.prev) {
				index -= 1;
			}
			points.splice(index, 2);
		} else {
			var prev = point.prev;
			var next = point.next;
			switch (prev.type) {
				case 'Q':
				case 'C':
					prev.type = 'L';
					break;
				case ' ':
					if (prev.prev.type == 'C') {
						prev.prev.type = 'Q';
					}
					break;
			}
			switch (next.type) {
				case 'Q':
					next.type = 'L';
					next.next.type = 'L';
					break;
				case 'C':
					next.type = 'L';
					next.next.type = 'Q';
					break;
				case ' ':
					next.type = 'L';
					if (point == 'C') {
						next.next.type = 'L';
					}
					break;
			}
			next.prev = prev;
			prev.next = next;
			points.splice(index, 1);
			canvas.removeChild(point.node);
		}
	},
	
	split: function(canvas, points, start, length, point) {
		var distance2 = Math.pow(7 * canvas.getScreenCTM().inverse().a, 2);
		var i = start;
		var n = start + length;
		do {
			switch(points[i].type) {
				case 'L':
					var prev = points[i].prev;
					var dx = points[i].x - prev.x;
					var dy = points[i].y - prev.y;
					var t = ((point.x - prev.x) * dx + (point.y - prev.y) * dy) / (dx * dx + dy * dy);
					if ((0 < t) && (t < 1)) {
						dx = point.x - prev.x - t * dx;
						dy = point.y - prev.y - t * dy;
						if (dx * dx + dy * dy < distance2) {
							var p = Path.point(point.x - dx, point.y - dy, 'L', prev, canvas);
							points.splice(i, 0, p);
							return p;
						}
					}
					i += 1;
					break;
				case 'Q':
					var bezier = [points[i].prev, points[i], points[i + 1]];
					var roots = Poly.cubic(Poly.polynomial3(Poly.bernstein(bezier, point)));
					for (var root = 0; root < roots.length; root++) {
						var t = roots[root];
						if ((0 < t) && (t < 1)) {
							var p = Poly.bezier2(bezier, t);
							var dx = point.x - p.x;
							var dy = point.y - p.y;
							if (dx * dx + dy * dy < distance2) {
								var x01 = bezier[0].x + (bezier[1].x - bezier[0].x) * t;
								var y01 = bezier[0].y + (bezier[1].y - bezier[0].y) * t;
								var x12 = bezier[1].x + (bezier[2].x - bezier[1].x) * t;
								var y12 = bezier[1].y + (bezier[2].y - bezier[1].y) * t;
								var p0 = Path.point(x01, y01, 'Q', bezier[0], canvas);
								var p1 = Path.point(p.x, p.y, ' ', p0, canvas);
								bezier[1].x = x12; bezier[1].node.setAttribute('x', x12);
								bezier[1].y = y12; bezier[1].node.setAttribute('y', y12);
								points.splice(i, 0, p0, p1);
								return p1;
							}
						}
					}
					i += 2;
					break;
				case 'C':
					var bezier = [points[i].prev, points[i], points[i + 1], points[i + 2]];
					var roots = Poly.finder(Poly.bernstein(bezier, point));
					for (var root = 0; root < roots.length; root++) {
						var t = roots[root];
						var p = Poly.bezier3(bezier, t);
						var dx = point.x - p.x;
						var dy = point.y - p.y;
						if (dx * dx + dy * dy < distance2) {
							var x01 = bezier[0].x + (bezier[1].x - bezier[0].x) * t;
							var y01 = bezier[0].y + (bezier[1].y - bezier[0].y) * t;
							var x12 = bezier[1].x + (bezier[2].x - bezier[1].x) * t;
							var y12 = bezier[1].y + (bezier[2].y - bezier[1].y) * t;
							var x23 = bezier[2].x + (bezier[3].x - bezier[2].x) * t;
							var y23 = bezier[2].y + (bezier[3].y - bezier[2].y) * t;
							var x0112 = x01 + (x12 - x01) * t;
							var y0112 = y01 + (y12 - y01) * t;
							var x1223 = x12 + (x23 - x12) * t;
							var y1223 = y12 + (y23 - y12) * t;
							var p0 = Path.point(x01, y01, 'C', bezier[0], canvas);
							var p1 = Path.point(x0112, y0112, ' ', p0, canvas);
							var p2 = Path.point(p.x, p.y, ' ', p1, canvas);
							bezier[1].x = x1223; bezier[1].node.setAttribute('x', x1223);
							bezier[1].y = y1223; bezier[1].node.setAttribute('y', y1223);
							bezier[2].x = x23; bezier[2].node.setAttribute('x', x23);
							bezier[2].y = y23; bezier[2].node.setAttribute('y', y23);
							points.splice(i, 0, p0, p1, p2);
							return p2;
						}
					}
					i += 3;
					break;
				default:
					i += 1; // just in case
					break;
			}
		} while(i < n);
		return null;
	},
	
	render: function(glyph, path, points) {
		var start = points[0].prev;
		var d = 'M' + start.x + ',' + start.y;
		var i = 0;
		var n = points.length;
		do {
			d += points[i].type + points[i].x + ',' + points[i].y;
			if (start == points[i]) {
				if (i + 1 < n) {
					start = points[i + 1].prev;
					d += 'zM' + start.x + ',' + start.y;
				}
			}
		} while(++i < n)
		d = d + 'z';
		path.setAttribute('d', d);
		glyph.node.setAttribute('d', d); // webkit bug: changes in 'd' don't show up until mouse hover, see http://code.google.com/p/typism/issues/detail?id=46
	},
	
	render_safe: function(glyph, path, points) {
		if (points.length > 0) {
			Path.render(glyph, path, points);
		} else {
			path.setAttribute('d', '');
			glyph.node.setAttribute('d', '');
		}
	},
	
	mode: function(path, circle, editable) {
		if (editable) {
			path.setAttribute('stroke', '#09f');
			path.setAttribute('fill', 'none');
			circle.setAttribute('visibility', 'visible');
		} else {
			path.setAttribute('stroke', 'none');
			path.setAttribute('fill', '#000');
			circle.setAttribute('visibility', 'hidden');
		}
	}
	
};

Utils = {
	
	agent: function() {
	// Adapted from jQuery
	// http://jquery.com/
		var name = navigator.userAgent.toLowerCase();
		return {
			mozilla: /mozilla/.test(name) && !/(compatible|webkit)/.test(name),
			msie: /msie/.test(name) && !/opera/.test(name),
			opera: /opera/.test(name),
			webkit: /webkit/.test(name),
			chrome : /chrome/.test(name)
		}
	}(),
	
	build: function(tag, parent, attributes) {
		var element = document.createElementNS('http://www.w3.org/2000/svg', tag);
		for (attribute in attributes) {
			switch (attribute) {
				case 'href':
					element.setAttributeNS('http://www.w3.org/1999/xlink', attribute, attributes[attribute]);
					break;
				default:
					element.setAttribute(attribute, attributes[attribute]);
					break;
			}
		}
		if (parent) {
			parent.appendChild(element);
		}
		return element;
	},
	
	position: function(canvas, event) {
		var point = canvas.ownerSVGElement.createSVGPoint();
		var matrix = canvas.getScreenCTM();
		point.x = event.clientX;
		point.y = event.clientY;
		return point.matrixTransform(matrix.inverse());
	},
	
	hit: function(x, y, canvas, points) {
		var d = 7 * canvas.getScreenCTM().inverse().a;
		var min_x = x - d;
		var max_x = x + d;
		var min_y = y - d;
		var max_y = y + d;
		var i = 0;
		var n = points.length;
		do {
			if ((min_x < points[i].x) && (points[i].x < max_x) && (min_y < points[i].y) && (points[i].y < max_y)) {
				return points[i];
			}
		} while(++i < n)
		return null;
	},
	
	filter: function(points, callback) {
		var result = [];
		var n = points.length;
		if (n > 0) {
			var i = 0;
			do {
				if (callback(points[i])) {
					result[result.length] = points[i];
				}
			} while(++i < n)
		}
		return result;
	},
	
	selected: function(point) {
		return point.selected;
	},
	
	deselect: function(point) {
		if (point.selected) {
			point.selected = false;
			point.node.setAttribute('fill', '#fff');
		}
	},
	
	invert: function(point) {
		if (point.selected) {
			point.selected = false;
			point.node.setAttribute('fill', '#fff');
		} else {
			point.selected = true;
			point.node.setAttribute('fill', '#09f');
		}
	},
	
	distance: function(a, b) {
		var dx = a.x - b.x;
		var dy = a.y - b.y;
		return Math.sqrt(dx * dx + dy * dy);
	}
	
};

Poly = {
	
	inside: function(contour, point) {
	// Adapted from "A Point About Polygons" by Bob Stein & Craig Yap, 6/19/95
	// http://www.visibone.com/inpoly/inpoly.c.txt
	// Note: In my testings this was about 20% faster than the method from "comp.graphics.algorithms FAQ, 2.03:
	// How do I find if a point lies within a polygon?" by Wm. Randolph Franklin
		var inside = false;
		var n = contour.length;
		var a = contour[n - 1];
		for (var i = 0; i < n; i++) {
			var b = contour[i];
			if (b.x > a.x) {
				if (
					(b.x < point.x) == (point.x <= a.x) &&
					(point.y - a.y) * (b.x - a.x) < (b.y - a.y) * (point.x - a.x)
				) {
					inside = !inside;
				}
			} else {
				if (
					(b.x < point.x) == (point.x <= a.x) &&
					(point.y - b.y) * (a.x - b.x) < (a.y - b.y) * (point.x - b.x)
				) {
					inside = !inside;
				}
			}
			a = b;
		}
		return inside;
	},
	
	orientation: function(contour) {
	// Adapted from "comp.graphics.algorithms FAQ, 2.07: How do I find the orientation of a simple polygon?"
	// by Joseph O'Rourke, 25 August 1995
	// http://maven.smith.edu/~orourke/Code/polyorient.C
	// Note: The exit test is from "Area of Triangles and Polygons (2D & 3D)" by Dan Sunday
		var b = contour[0];
		var n = contour.length;
		for (var i = 1; i < n; i++) {
			var p = contour[i];
			if ((p.y < b.y) || ((p.y == b.y) && (p.x > b.x))) { // Note: SVG y axis pointing downwards (but not in SVG fonts)
				b = p;
			}
		}
		var a = b.prev;
		var c = b.next;
		return (b.x - a.x) * (c.y - a.y) - (c.x - a.x) * (b.y - a.y);
	},
	
	bezier2: function(segment, t) {
		var u = 1 - t;
		var t2 = t * t;
		var u2 = u * u;
		return {
			x: u2*segment[0].x + 2*t*u*segment[1].x + t2*segment[2].x,
			y: u2*segment[0].y + 2*t*u*segment[1].y + t2*segment[2].y
		}
	},
	
	bezier3: function(segment, t) {
		var u = 1 - t;
		var t2 = t * t;
		var u2 = u * u;
		var t3 = t2 * t;
		var u3 = u2 * u;
		return {
			x: u3*segment[0].x + 3*t*u2*segment[1].x + 3*t2*u*segment[2].x + t3*segment[3].x,
			y: u3*segment[0].y + 3*t*u2*segment[1].y + 3*t2*u*segment[2].y + t3*segment[3].y
		}
	},
	
	bezier5y: function(ys, t) {
		var u = 1 - t;
		var t2 = t * t;
		var u2 = u * u;
		var t3 = t2 * t;
		var u3 = u2 * u;
		var t4 = t3 * t;
		var u4 = u3 * u;
		return u4*u*ys[0] + 5*u4*t*ys[1] + 10*u3*t2*ys[2] + 10*u2*t3*ys[3] + 5*u*t4*ys[4] + t4*t*ys[5];
	},
	
	polynomial3: function(ys) {
		return [
			-ys[0] + 3*ys[1] - 3*ys[2] + ys[3],
			3*ys[0] - 6*ys[1] + 3*ys[2],
			-3*ys[0] + 3*ys[1],
			ys[0]
		];
	},
	
	zero: function(value) {
	// Adapted from "Cubic and Quartic Roots" by Jochen Schwarze in "Graphics Gems", Academic Press, 1990
	// http://tog.acm.org/GraphicsGems/gems/Roots3And4.c
		return (-1e-9 < value) && (value < 1e-9);
	},
	
	cbrt: function(value) {
	// ditto
		if (value > 0) {
			return Math.pow(value, 1 / 3);
		} else if (value < 0) {
			return -Math.pow(-value, 1 / 3);
		} else {
			return 0;
		}
	},
	
	cubic: function(polynomial) {
	// ditto
	// Note: polynomial[0]*x^3 + polynomial[1]*x^2 + polynomial[2]*x + polynomial[3] = 0
		var roots;
		var a = polynomial[1] / polynomial[0];
		var b = polynomial[2] / polynomial[0];
		var c = polynomial[3] / polynomial[0];
		var a2 = a * a;
		var p = 1 / 3 * (-1 / 3 * a2 + b);
		var q = 1 / 2 * (2 / 27 * a * a2 - 1 / 3 * a * b + c);
		var p3 = p * p * p;
		var d2 = q * q + p3;
		if (Poly.zero(d2)) {
			if (Poly.zero(q)) {
				roots = [0];
			} else {
				var u = Poly.cbrt(-q);
				roots = [2 * u, -u];
			}
		} else if (d2 < 0) {
			var phi = 1 / 3 * Math.acos(-q / Math.sqrt(-p3));
			var t = 2 * Math.sqrt(-p);
			roots = [t * Math.cos(phi), -t * Math.cos(phi + Math.PI / 3), -t * Math.cos(phi - Math.PI / 3)];
		} else {
			var d = Math.sqrt(d2);
			var u = Poly.cbrt(d - q);
			var v = -Poly.cbrt(d + q);
			roots = [u + v];
		}
		var sub = 1 / 3 * a;
		for (var i = 0, n = roots.length; i < n; ++i) {
			roots[i] -= sub;
		}
		return roots;
	},
	
	finder: function(polynomial) {
	// http://en.wikipedia.org/wiki/False_position_method
	// Note: In my testings this was about 40x faster than the root finder from "Solving the Nearest Point-on-Curve
	// Problem" by Philip J. Schneider
		var degree = polynomial.length - 1;
		var roots = [];
		var i = 1;
		var side = 0;
		var left, mid, right, f_left, f_mid, f_right;
		var old_left, old_f_left;
		old_left = 0.0;
		old_f_left = polynomial[0];
		do {
			left = old_left;
			f_left = old_f_left;
			if (i == degree) {
				right = 1.0;
				f_right = polynomial[degree];
			}
			else {
				right = i / degree;
				f_right = Poly.bezier5y(polynomial, right);
			}
			old_left = right;
			old_f_left = f_right;
			if (f_left * f_right < 0) {
				do {
					mid = right - f_right * (right - left) / (f_right - f_left);
					f_mid = Poly.bezier5y(polynomial, mid);
					var mul = f_left * f_mid;
					if (mul > 0) {
						left = mid;
						f_left = f_mid;
						if (side == -1) {
							f_right *= 0.5;
						}
						else {
							side = -1;
						}
					}
					else if (mul < 0) {
						right = mid;
						f_right = f_mid;
						if (side == 1) {
							f_left *= 0.5;
						}
						else {
							side = 1;
						}
					}
					else {
						break;
					}
				} while (right - left > 1e-3)
				roots[roots.length] = mid;
			}
		} while (i++ < degree)
		return roots;
	},
	
	z2: [
		[1, 2 / 3, 1 / 3],
		[1 / 3, 2 / 3, 1]
	],
	
	z3: [
		[1.0, 0.6, 0.3, 0.1],
		[0.4, 0.6, 0.6, 0.4],
		[0.1, 0.3, 0.6, 1.0]
	],
	
	bernstein: function(segment, point) {
	// Adapted from "Solving the Nearest Point-on-Curve Problem" by Philip J. Schneider in "Graphics Gems", Academic Press, 1990
	// Note: Errata p. 610: the binomial "( n-i [over] j )" should be "( n-1 [over] j )".
	// This error appears on the fifth line of the long derivation and within the Zi,j definition.
		var degree = segment.length - 1;
		var c = new Array(degree + 1);
		var d = new Array(degree);
		var cd = new Array(degree);
		var w = new Array(degree + degree - 1 + 1);
		var z = degree == 2 ? Poly.z2 : Poly.z3;
		for (var i = 0; i <= degree; i++) {
			c[i] = [segment[i].x - point.x, segment[i].y - point.y];
		}
		for (var i = 0; i <= degree - 1; i++) {
			d[i] = [degree * (segment[i + 1].x - segment[i].x), degree * (segment[i + 1].y - segment[i].y)];
		}
		for (var j = 0; j <= degree - 1; j++) {
			cd[j] = new Array(degree + 1);
			for (var i = 0; i <= degree; i++) {
				cd[j][i] = c[i][0] * d[j][0] + c[i][1] * d[j][1]
			}
		}
		for (var i = 0; i <= degree + degree - 1; i++) {
			w[i] = 0;
		}
		for (var k = 0; k <= degree + degree - 1; k++) {
			var lb = Math.max(0, k - degree + 1);
			var ub = Math.min(k, degree);
			for (var i = lb; i <= ub; i++) {
				var j = k - i;
				w[i + j] += cd[j][i] * z[j][i];
			}
		}
		return w;
	},
	
};


document.addEventListener('DOMContentLoaded', Editor.init, false);
